﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: AccessStorage.cs
//
//  Description: Provides a storage class for Microsoft Access.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Data;
using System.Data.OleDb;
using System.IO;
using System.Runtime.InteropServices;
using Chili.Opf3.Query;
using System.Security.Permissions;

namespace Chili.Opf3.Storages.OleDb
{
	/// <summary>Storage class that encapsulates a pyhsical Ms Access database.</summary>
	/// <remarks>
	/// 	<para>This class encapsulates a physical Ms Access database. It is used to access to
	///     an Oracle database that is installed somewhere on your or on a remote machine. The storage
	///     implements the basic interfaces, such as
	///     <see cref="Opf3.Storages.IStorage">IStorage</see> for the basic storage IO
	///     functinality and
	///     <see cref="Opf3.Storages.ITransactionStorage">ITransactionStorage</see> for the
	///     transaction management.</para>
	/// 	<para><b>Attention:</b> You should not access the methods of the storage directly
	///     but instead register the storage with your instances of the
	///     <see cref="Opf3.ObjectContext">ObjectContext</see>. The
	///     <see cref="Opf3.ObjectContext">ObjectContext</see> accesses the methods internally
	///     to get the objects from the storage.</para>
	/// </remarks>
	/// <example>
	/// The following example shows how you should use the Ms Access storage in your 
	/// applications.
	/// <code lang="CS">
	/// // ... Other code
	/// 
	/// // Creates a new ObjectContext that uses an Ms Access database as storage.
	/// // You should only pass an instance of the storage to your ObjectContext.
	/// // That's it! Nothing more. The methods of the storage are accessed
	/// // by the context internally.
	/// ObjectContext context = new ObjectContext(
	///		new AccessStorage("... some ConnectionString ..."));
	/// 
	/// // ... Other code
	/// </code>
	/// </example>
	public class AccessStorage : OleDbStorage, ICustomDataTypesStorage
	{
		private const string _constraintError = "3316";
		private const string _foreignKeyViolation = "3201";
		private const string _primaryKeyViolation = "3022";
		private const string _indexViolation = "2601";

		/// <summary>
		/// Creates a new instance of the <see cref="AccessStorage">AccessStorage
		/// Class</see>.
		/// </summary>
		/// <param name="connectionString">
		/// Specifies a string that contains the information (for ADO.NET) to connect to
		/// storage.
		/// </param>
		public AccessStorage(string connectionString) : base(connectionString)
		{
        }

		/// <summary>
		/// Creates a new instance of the <see cref="AccessStorage">AccessStorage
		/// Class</see>.
		/// </summary>
		/// <param name="path">Specifies the path of the database.</param>
		/// <param name="fileName">Specifies the file name of the database.</param>
        public AccessStorage(string path, string fileName) : 
			this(string.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\\{1};", path, fileName))
        {
			if (path == null)
				throw new ArgumentNullException("path");
			if (fileName == null)
				throw new ArgumentNullException("fileName");
        }

		/// <summary>
		/// Executes a command on the storage that does not expect any results. It the method
		/// returns an argument it is used by the framework to set the
		/// <see cref="Opf3.FieldAttribute.AutoNumber">AutoNumber</see> property of the persistent
		/// object.
		/// </summary>
		/// <param name="query">
		/// 	<see cref="Opf3.Query.IQuery">IQuery</see> that is compiled and executed on the
		/// storage.
		/// </param>
		/// <exception cref="Opf3.Storages.StorageConstraintException">A constraint of the storage 
		/// has been violated.</exception>
		/// <exception cref="Opf3.Storages.ForeignKeyViolationException">A foreign key in the storage 
		/// has been violated.</exception>
		/// <exception cref="Opf3.Storages.PrimaryKeyViolationException">A duplicate primary key 
		/// is inserted in the storage.</exception>
		/// <exception cref="Opf3.Storages.PrimaryKeyViolationException">An index has been violated.</exception>
		/// <exception cref="Opf3.Storages.StorageException">Any other unexpected storage exception.</exception>
		/// <exception cref="ArgumentNullException">Query is null.</exception>
		public override object ExecuteCommand(IQuery query)
		{
			if (query == null)
				throw new ArgumentNullException("query");

			try
            {
				object autoValue = null;
				CompiledQuery compiledQuery = query.ToCompiledQuery(this);
				Tracing.Current.TraceStorageIO(compiledQuery);

				// Prepare the command for execution.
				using (IDbCommand dbCommand = this.PrepareCommand(compiledQuery))
				{
					dbCommand.ExecuteNonQuery();

					if (query.QueryType == QueryTypes.Insert)
					{
						// Check if the query's parameter contain an autonumber parameter.
						if (compiledQuery.AutoNumberItems != null)
						{
							OleDbCommand idCommand = new OleDbCommand("SELECT @@IDENTITY", (OleDbConnection)dbCommand.Connection);
							idCommand.Transaction = (OleDbTransaction)dbCommand.Transaction;
							autoValue = idCommand.ExecuteScalar();

							Tracing.Current.TraceStorageIO("Autonumber value returned: " + autoValue);
						}
					}
					// If no transaction is active we can close the connection after executing the command.
					if (!IsTransactionActive)
						dbCommand.Connection.Close();
				}
				return autoValue;
			}
            catch (OleDbException ex)
            {
				if (ex.Errors[0].SQLState == _constraintError)
					throw new StorageConstraintException(ex.Message, ex, query);
				else if (ex.Errors[0].SQLState == _foreignKeyViolation)
					throw new ForeignKeyViolationException(ex.Message, ex, query);
				else if (ex.Errors[0].SQLState == _primaryKeyViolation)
					throw new PrimaryKeyViolationException(ex.Message, ex, query);
				else if (ex.Errors[0].SQLState == _indexViolation)
					throw new IndexViolationException(ex.Message, ex, query);
				else
					throw new StorageException(ex.Message, ex, query);
			}
		}

		#region CompactDatabase

		/// <summary>
		/// Repairs and compresses the database using the Ms Access build in functionalities.
		/// </summary>
		/// <exception cref="System.IO.FileNotFoundException">The database file couldn't be found.</exception>
		/// <exception cref="System.IO.IOException">The database file has been read-only.</exception>
		[SecurityPermission(SecurityAction.Demand)]
		public void CompactDatabase()
		{
			// Create an instance of a Jet Replication Object.
			object objJRO = Activator.CreateInstance(Type.GetTypeFromProgID("JRO.JetEngine"));

			string target = Path.GetTempFileName();
			if (File.Exists(target))
				File.Delete(target);

			// Exctract Jet filename from the ConnectionString
			string dataTag = "DATA SOURCE=";
			int i1 = ConnectionString.ToUpper().IndexOf(dataTag) + dataTag.Length;
			int i2 = ConnectionString.IndexOf(";", i1);

			string source = ConnectionString.Substring(i1, i2 - i1);

			// Check if file exists.
			if (!File.Exists(source))
				throw new FileNotFoundException(string.Format("The file '{0}' do not exists!", source));

			// Check if file is ReadOnly.
			if ((File.GetAttributes(source) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
				throw new IOException(string.Format("The file '{0}' is read only!", source));

			object[] oParams = new object[] { ConnectionString,
				string.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Jet OLEDB:Engine Type=5", target)};

			// Invoke the CompactDatabase method of a JRO object.
			objJRO.GetType().InvokeMember("CompactDatabase", System.Reflection.BindingFlags.InvokeMethod,
				null, objJRO, oParams);

			// Database is compacted now to a new file target.
			File.Delete(source);
			File.Move(target, source);

			// Release the COM object.
			Marshal.ReleaseComObject(objJRO);
		}

		#endregion

        #region ICustomDataTypesStorage Members

        /// <summary>
        /// Allows to convert from the type in the storage to the target type of the object.
        /// </summary>
        /// <param name="value">Value loaded from the storage.</param>
        /// <param name="targetType">The type of the member of the persistent object.</param>
        /// <returns>The value converted to the target type.</returns>
        object ICustomDataTypesStorage.ChangeFromDataType(object value, Type targetType)
        {
			if (value == null || targetType is IUserDefinedType)
				return value;

            // If the type does not match the type of the persistent object we have to change it.
            // Convert to the member type if necessary (Enums are not converted).
            if (value != DBNull.Value && value.GetType() != targetType && !targetType.IsSubclassOf(typeof(Enum)))
                value = Convert.ChangeType(value, targetType);

            return value;
        }

        /// <summary>
        /// Allows to convert from the type of the object to a type that is supported by the storage.
        /// </summary>
        /// <param name="value">Value from the persistent object that is converted to a type supported by the storage.</param>
        /// <returns>The value converted to a type that is supported by the storage.</returns>
        object ICustomDataTypesStorage.ChangeToDataType(object value)
        {
            // When converting back to data type we don't need to change the value anymore.
            return value;
        }

        #endregion
    }
}
